package com.inspinia.base.util;


import com.inspinia.base.common.model.BaseConstants;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.protocol.HttpClientContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 真正的下载线程，该线程用于执行该线程所要负责下载的数据。
 *
 * @author wangjie
 * @version 创建时间：2013-2-7 上午11:58:24
 */
public class FileDownloadThread extends Thread {

    /**
     * 日志对象
     */
    protected static Logger logger = LoggerFactory.getLogger(FileDownloadThread.class);
    private final int readTimeout;
    private final int connectTimeout;
    private final boolean reDownloadAfterGotReadTimeout;

    private String url;
    private File tempFile;
    private long startPos;
    /**
     * 此线程需要下载的数据长度。
     */
    public long endPos;

    public long length;

    private FileDownloaderNotificationThread notificationThread;
    private boolean isRunning = true;
    //多线程同步辅助
    private AtomicInteger threadCount;

    private static final int BUFF_LENGTH = 1024;
    private String RANGE_PROPERTY = "RANGE";
    private long downloadedLength;
    private final FileDownloader fileDownloader;
    //超时的异常信息
    public static final String TIMEOUT_ERROR_MSG = "timed out";
    //连接被拒绝的异常信息
    public static final String CONNECTION_EFUSED_ERROR_MSG = "Connection refused";


    /**
     * 构造方法，可生成配置完整的JFileDownloadThread对象
     *
     * @param fileDownloader
     * @param url                           要下载的目标文件URL
     * @param tempFile                      要保存的目标文件
     * @param startPos                      该线程需要下载目标文件第几个byte之后的数据
     * @param endPos                        该线程需要下载多少长度的数据
     * @param notificationThread            通知进度线程
     * @param threadCount                   已完成线程数量
     * @param readTimeout
     * @param connectTimeout
     * @param reDownloadAfterGotReadTimeout
     */
    public FileDownloadThread(FileDownloader fileDownloader, String url, File tempFile, long startPos,
                              long endPos, FileDownloaderNotificationThread notificationThread, AtomicInteger threadCount, int readTimeout, int connectTimeout, boolean reDownloadAfterGotReadTimeout) {
        this.url = url;
        this.tempFile = tempFile;
        this.startPos = startPos;
        this.endPos = endPos;
        this.notificationThread = notificationThread;
        this.threadCount = threadCount;
        this.length = endPos - startPos;
        this.fileDownloader = fileDownloader;
        this.readTimeout = readTimeout;
        this.connectTimeout = connectTimeout;
        this.reDownloadAfterGotReadTimeout = reDownloadAfterGotReadTimeout;
    }

    /**
     * 该方法将执行下载功能，并把数据存储在目标文件中的相应位置。
     */
    @Override
    public void run() {
        while (isRunning) {
            if (logger.isDebugEnabled()) {
                logger.debug("线程正在下载文件");
            }
            download();
        }
    }

    //重试的计数器
    private int retryCount = 0;
    //重试的次数限制
    public static final int RETRY_LIMIT = 10;

    /**
     * 下载文件
     */
    private void download() {
        CloseableHttpResponse response = null;
        RandomAccessFile raf = null;
        try {
            //获取文件输入流，读取文件内容
            raf = new RandomAccessFile(tempFile.getAbsolutePath(), "rw");
            raf.seek(startPos);
            response = getUrlResponse();

            int stateCode = response.getStatusLine().getStatusCode();
            if (stateCode == HttpURLConnection.HTTP_OK || stateCode == HttpURLConnection.HTTP_PARTIAL) {

                writeContent2file(response.getEntity().getContent(), raf);
            } else {

                logger.info("url" + url + "状态码不是200或206！正在尝试第" + retryCount + "次重新下载");
                if (!reDownload()) {
                    notifyMainThreadStop(new HttpStateCodeNot200Or206Exception(this.url));
                }
            }
        } catch (IOException e) {
            if (isRetryNeed(e.getMessage())) {

                //如果是读取超时的话，就再次下载。直到不超时为止。注意区分读取超时和连接超时，连接超时可能是服务器挂了或拒绝访问，就不用再次下载。
                logger.info("url" + url + "读取超时！正在尝试第" + retryCount + "次重新下载");
                if (reDownload()) {
                    //重新下载成功则不用停止本次下载。
                    return;
                }
            }
            //通知线程本次下载失败。
            notifyMainThreadStop(e);
        } finally {
            cancelThread();
            threadCount.incrementAndGet();
            IOUtils.closeQuietly(raf);
            IOUtils.closeQuietly(response);
        }
    }

    /**
     * 判断是否需要重新下载
     *
     * @param errorMessage
     * @return
     */
    private boolean isRetryNeed(String errorMessage) {
        return (errorMessage.contains(TIMEOUT_ERROR_MSG)
                || errorMessage.contains(CONNECTION_EFUSED_ERROR_MSG))
                && reDownloadAfterGotReadTimeout;
    }

    private boolean reDownload() {
        if (retryCount > RETRY_LIMIT) {
            logger.info("url" + url + "重新下载超出限制");
            return false;
        }
        retryCount++;
        ThreadHelper.sleep((int) BaseConstants.ONE_MINUTE_MS);
        download();
        return true;
    }

    private void writeContent2file(InputStream is, RandomAccessFile raf) throws IOException {


        byte[] buff = new byte[BUFF_LENGTH];
        int len;
        while ((len = is.read(buff)) > 0 && downloadedLength < this.length && isRunning) {

            if (logger.isDebugEnabled()) {
                logger.debug("线程正在从流中读取文件");
            }

            //最后一次会读取buff的长度的数据，但是由于是分段的，其实只需要长度到endPos就可以了。
            if (downloadedLength + len > this.length) {
                raf.write(buff, 0, (int) (this.length - downloadedLength));
                downloadedLength = this.length;
                break;
            } else {
                raf.write(buff, 0, len);
                downloadedLength += len;
            }
        }
    }

    private CloseableHttpResponse getUrlResponse() throws IOException {
        HttpGet httpGet = new HttpGet(url);
        HttpClientUtil.setSimulateHeader(httpGet);
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(connectTimeout)
                .setConnectTimeout(connectTimeout).setSocketTimeout(readTimeout).build();
        httpGet.setConfig(requestConfig);
        //设置只取某一段的文件内容。如果设置了结束为止，可能会报Premature end of Content-Length delimited message body的错误
        httpGet.setHeader(RANGE_PROPERTY, "bytes=" + startPos + "-");
        return HttpClientUtil.getHttpClient().execute(httpGet,
                HttpClientContext.create());
    }

    /**
     * 通知主线程本次下载失败
     *
     * @param e
     */
    private void notifyMainThreadStop(Exception e) {
        logger.error("", e);
        //通知线程本次下载失败。
        notificationThread.notificationError(e);
        fileDownloader.cancelDownload();
    }

    /**
     * 获取用于当前已经下载到的开始位置
     *
     * @return
     */
    public long getCurrStartPos() {
        return startPos += downloadedLength;
    }

    public long getStartPos() {
        return startPos;
    }

    public long getEndPos() {
        return endPos;
    }

    public long getDownloadedLength() {
        return downloadedLength;
    }

    /**
     * 取消该线程下载
     *
     * @author wangjie
     */
    public void cancelThread() {
        isRunning = false;
    }


}